/**
 * Copyright (C) 2009 eXo Platform SAS.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.exoplatform.application.registry.impl;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.chromattic.api.ChromatticSession;
import org.exoplatform.application.gadget.Gadget;
import org.exoplatform.application.gadget.GadgetRegistryService;
import org.exoplatform.application.registry.Application;
import org.exoplatform.application.registry.ApplicationCategoriesPlugins;
import org.exoplatform.application.registry.ApplicationCategory;
import org.exoplatform.application.registry.ApplicationRegistryService;
import org.exoplatform.commons.chromattic.ChromatticLifeCycle;
import org.exoplatform.commons.chromattic.ChromatticManager;
import org.exoplatform.container.ExoContainer;
import org.exoplatform.container.ExoContainerContext;
import org.exoplatform.container.component.ComponentPlugin;
import org.exoplatform.container.component.RequestLifeCycle;
import org.exoplatform.services.security.MembershipEntry;
import org.exoplatform.portal.config.UserACL;
import org.exoplatform.portal.config.model.ApplicationType;
import org.exoplatform.portal.pom.config.POMSessionManager;
import org.exoplatform.portal.pom.spi.portlet.Portlet;
import org.exoplatform.portal.pom.spi.wsrp.WSRP;
import org.gatein.common.i18n.LocalizedString;
import org.gatein.common.logging.Logger;
import org.gatein.common.logging.LoggerFactory;
import org.gatein.mop.api.content.ContentType;
import org.gatein.mop.api.content.Customization;
import org.gatein.pc.api.PortletInvoker;
import org.gatein.pc.api.info.MetaInfo;
import org.gatein.pc.api.info.PortletInfo;
import org.picocontainer.Startable;

/**
 * The fundamental reason that motives to use tasks is because of the JMX access that does not setup a context and therefore the
 * task either reuse the existing context setup by the portal or create a temporary context when accessed by JMX.
 *
 * @author <a href="mailto:julien.viet@exoplatform.com">Julien Viet</a>
 * @version $Revision$
 */
public class ApplicationRegistryServiceImpl implements ApplicationRegistryService, Startable {

    /** . */
    private static final String INTERNAL_PORTLET_TAG = "gatein_internal";

    /** . */
    private static final String REMOTE_CATEGORY_NAME = "remote";

    /** . */
    private List<ApplicationCategoriesPlugins> plugins;

    /** . */
    private final ChromatticManager manager;

    /** . */
    private final ChromatticLifeCycle lifeCycle;

    /** . */
    private final Logger log = LoggerFactory.getLogger(ApplicationRegistryServiceImpl.class);

    /** . */
    final POMSessionManager mopManager;

    /** Should match WSRPPortletInfo.PRODUCER_NAME_META_INFO_KEY */
    private static final String PRODUCER_NAME_META_INFO_KEY = "producer-name";

    /*
     * MIN and MAX values similar to StringLengthValidator.
     *
     * @see org.exoplatform.applicationregistry.webui.component.UIApplicationForm
     */
    private static final int MIN = 3;
    private static final int MAX = 30;

    public static final String PRODUCER_CATEGORY_NAME_SUFFIX = " Producer";

    private UserACL acl;
    private String anyOfAdminGroup;

    public ApplicationRegistryServiceImpl(ChromatticManager manager, POMSessionManager mopManager, UserACL userACL) {

        ApplicationRegistryChromatticLifeCycle lifeCycle = (ApplicationRegistryChromatticLifeCycle) manager.getLifeCycle("app");
        lifeCycle.registry = this;

        //
        this.manager = manager;
        this.lifeCycle = lifeCycle;
        this.mopManager = mopManager;
        this.acl = userACL;
        this.anyOfAdminGroup = new MembershipEntry(acl.getAdminGroups()).toString();
    }

    public ContentRegistry getContentRegistry() {
        ChromatticSession session = lifeCycle.getChromattic().openSession();
        ContentRegistry registry = session.findByPath(ContentRegistry.class, "app:applications");
        if (registry == null) {
            registry = session.insert(ContentRegistry.class, "app:applications");
        }
        return registry;
    }

    public void initListener(ComponentPlugin com) {
        if (com instanceof ApplicationCategoriesPlugins) {
            if (plugins == null) {
                plugins = new ArrayList<ApplicationCategoriesPlugins>();
            }
            plugins.add((ApplicationCategoriesPlugins) com);
        }
    }

    public List<ApplicationCategory> getApplicationCategories(final Comparator<ApplicationCategory> sortComparator,
            String accessUser, final ApplicationType<?>... appTypes) {
        final List<ApplicationCategory> categories = new ArrayList<ApplicationCategory>();

        //
        ContentRegistry registry = getContentRegistry();

        //
        for (CategoryDefinition categoryDef : registry.getCategoryList()) {
            ApplicationCategory category = load(categoryDef, appTypes);
            categories.add(category);
        }

        //
        if (sortComparator != null) {
            Collections.sort(categories, sortComparator);
        }

        //
        return categories;
    }

    public List<ApplicationCategory> getApplicationCategories(String accessUser, ApplicationType<?>... appTypes)
            throws Exception {
        return getApplicationCategories(null, accessUser, appTypes);
    }

    public List<ApplicationCategory> getApplicationCategories() throws Exception {
        return getApplicationCategories(null);
    }

    public List<ApplicationCategory> getApplicationCategories(Comparator<ApplicationCategory> sortComparator) throws Exception {
        return getApplicationCategories(sortComparator, null);
    }

    public ApplicationCategory getApplicationCategory(final String name) {
        ContentRegistry registry = getContentRegistry();

        //
        CategoryDefinition categoryDef = registry.getCategory(name);
        if (categoryDef != null) {
            ApplicationCategory applicationCategory = load(categoryDef);
            return applicationCategory;
        }

        //
        return null;
    }

    public void save(final ApplicationCategory category) {
        ContentRegistry registry = getContentRegistry();

        //
        String categoryName = category.getName();

        //
        CategoryDefinition categoryDef = registry.getCategory(categoryName);
        if (categoryDef == null) {
            categoryDef = registry.createCategory(categoryName);
        }

        //
        categoryDef.setDisplayName(category.getDisplayName());
        categoryDef.setCreationDate(category.getCreatedDate());
        categoryDef.setLastModificationDate(category.getModifiedDate());
        categoryDef.setDescription(category.getDescription());
        categoryDef.setAccessPermissions(category.getAccessPermissions());
    }

    public void remove(final ApplicationCategory category) {
        ContentRegistry registry = getContentRegistry();
        registry.getCategoryMap().remove(category.getName());
    }

    public List<Application> getApplications(ApplicationCategory category, ApplicationType<?>... appTypes) throws Exception {
        return getApplications(category, null, appTypes);
    }

    public List<Application> getApplications(final ApplicationCategory category, final Comparator<Application> sortComparator,
            final ApplicationType<?>... appTypes) {
        ContentRegistry registry = getContentRegistry();

        //
        CategoryDefinition categoryDef = registry.getCategory(category.getName());
        List<Application> applications = load(categoryDef, appTypes).getApplications();

        //
        if (sortComparator != null) {
            Collections.sort(applications, sortComparator);
        }

        //
        return applications;
    }

    public List<Application> getAllApplications() throws Exception {
        List<Application> applications = new ArrayList<Application>();
        List<ApplicationCategory> categories = getApplicationCategories();
        for (ApplicationCategory category : categories) {
            applications.addAll(getApplications(category));
        }
        return applications;
    }

    public Application getApplication(String id) throws Exception {
        String[] fragments = id.split("/");
        if (fragments.length < 2) {
            throw new Exception("Invalid Application Id: [" + id + "]");
        }

        String category = fragments[0];
        String applicationName = fragments[1];

        // If the application name contained a beginning slash (which can happen with WSRP), we need to hack around the
        // hardcoding of portlet id expectations >_<
        if (fragments.length == 3 && applicationName.length() == 0) {
            applicationName = "/" + fragments[2];
        }
        return getApplication(category, applicationName);
    }

    public Application getApplication(final String category, final String name) {
        ContentRegistry registry = getContentRegistry();

        //
        CategoryDefinition categoryDef = registry.getCategory(category);
        if (categoryDef != null) {
            ContentDefinition contentDef = categoryDef.getContentMap().get(name);
            if (contentDef != null) {
                return load(contentDef);
            }
        }

        //
        return null;
    }

    public void save(final ApplicationCategory category, final Application application) {
        ContentRegistry registry = getContentRegistry();

        //
        String categoryName = category.getName();
        CategoryDefinition categoryDef = registry.getCategory(categoryName);
        if (categoryDef == null) {
            categoryDef = registry.createCategory(categoryName);
            save(category, categoryDef);
        }

        //
        ContentDefinition contentDef = null;
        CategoryDefinition applicationCategoryDef = registry.getCategory(application.getCategoryName());
        String applicationName = application.getApplicationName();
        if (applicationCategoryDef != null) {
            contentDef = applicationCategoryDef.getContentMap().get(applicationName);
        }
        if (contentDef == null) {
            String contentId = application.getContentId();
            ContentType<?> contentType = application.getType().getContentType();
            String definitionName = application.getApplicationName();
            contentDef = categoryDef.createContent(definitionName, contentType, contentId);
        } else {
            // A JCR move actually
            categoryDef.getContentList().add(contentDef);
        }

        // Update state
        save(application, contentDef);
    }

    public void update(final Application application) {
        ContentRegistry registry = getContentRegistry();

        //
        String categoryName = application.getCategoryName();
        CategoryDefinition categoryDef = registry.getCategory(categoryName);
        if (categoryDef == null) {
            throw new IllegalStateException();
        }

        //
        ContentDefinition contentDef = categoryDef.getContentMap().get(application.getApplicationName());
        if (contentDef == null) {
            throw new IllegalStateException();
        }

        // Update state
        save(application, contentDef);
    }

    public void remove(final Application app) {
        if (app == null) {
            throw new NullPointerException();
        }

        //
        ContentRegistry registry = getContentRegistry();

        //
        String categoryName = app.getCategoryName();
        CategoryDefinition categoryDef = registry.getCategory(categoryName);

        //
        if (categoryDef != null) {

            String contentName = app.getApplicationName();
            categoryDef.getContentMap().remove(contentName);
        }
    }

    public void importExoGadgets() throws Exception {
        ContentRegistry registry = getContentRegistry();

        //
        ExoContainer container = ExoContainerContext.getCurrentContainer();
        GadgetRegistryService gadgetService = (GadgetRegistryService) container
                .getComponentInstanceOfType(GadgetRegistryService.class);
        List<Gadget> eXoGadgets = gadgetService.getAllGadgets();

        //
        if (eXoGadgets != null) {
            ArrayList<String> permissions = new ArrayList<String>();
            permissions.add(anyOfAdminGroup);
            String categoryName = "Gadgets";

            //
            CategoryDefinition category = registry.getCategory(categoryName);
            if (category == null) {
                category = registry.createCategory(categoryName);
                category.setDisplayName(categoryName);
                category.setDescription(categoryName);
                category.setAccessPermissions(permissions);
            }

            //
            for (Gadget ele : eXoGadgets) {
                ContentDefinition app = category.getContentMap().get(ele.getName());
                if (app == null) {
                    app = category.createContent(ele.getName(), org.exoplatform.portal.pom.spi.gadget.Gadget.CONTENT_TYPE,
                            ele.getName());
                    app.setDisplayName(ele.getTitle());
                    app.setDescription(ele.getDescription());
                    app.setAccessPermissions(permissions);
                }
            }
        }
    }

    public void importAllPortlets() throws Exception {
        ContentRegistry registry = getContentRegistry();

        //
        log.info("About to import portlets in application registry");

        //
        ExoContainer manager = ExoContainerContext.getCurrentContainer();
        PortletInvoker portletInvoker = (PortletInvoker) manager.getComponentInstance(PortletInvoker.class);
        Set<org.gatein.pc.api.Portlet> portlets = portletInvoker.getPortlets();

        //
        portlet: for (org.gatein.pc.api.Portlet portlet : portlets) {
            PortletInfo info = portlet.getInfo();
            String portletApplicationName = info.getApplicationName();
            String portletName = portlet.getContext().getId();

            // Need to sanitize portlet and application names in case they contain characters that would
            // cause an improper Application name
            portletApplicationName = sanitizePortletApplicationName(portletApplicationName);
            portletName = sanitizePortletName(portletName);

            MetaInfo metaInfo = portlet.getInfo().getMeta();
            LocalizedString keywordsLS = metaInfo.getMetaValue(MetaInfo.KEYWORDS);

            //
            Set<String> categoryNames = new HashSet<String>();

            // Process keywords
            if (keywordsLS != null) {
                String keywords = keywordsLS.getDefaultString();
                if (keywords != null && keywords.length() != 0) {
                    for (String categoryName : keywords.split(",")) {
                        // Trim name
                        categoryName = categoryName.trim();
                        if (INTERNAL_PORTLET_TAG.equalsIgnoreCase(categoryName)) {
                            log.debug("Skipping portlet (" + portletApplicationName + "," + portletName
                                    + ") + tagged as internal");
                            continue portlet;
                        } else {
                            categoryNames.add(categoryName);
                        }
                    }
                }
            }

            ArrayList<String> permissions = new ArrayList<String>();
            permissions.add(anyOfAdminGroup);
            // If no keywords, use the portlet application name
            if (categoryNames.isEmpty()) {
                categoryNames.add(portletApplicationName.trim());
            }

            // Additionally categorise the portlet as remote
            boolean remote = portlet.isRemote();
            if (remote) {
                categoryNames.add(REMOTE_CATEGORY_NAME);

                // add producer name to categories for easier finding of portlets for GTNPORTAL-823
                LocalizedString producerNameLS = metaInfo.getMetaValue(PRODUCER_NAME_META_INFO_KEY);
                if (producerNameLS != null) {
                    categoryNames.add(producerNameLS.getDefaultString() + PRODUCER_CATEGORY_NAME_SUFFIX);
                }
            }

            //
            log.info("Importing portlet (" + portletApplicationName + "," + portletName + ") in categories " + categoryNames);

            // Process category names
            for (String categoryName : categoryNames) {
                categoryName = sanitizeCategoryName(categoryName);
                CategoryDefinition category = registry.getCategory(categoryName);

                //
                if (category == null) {
                    category = registry.createCategory(categoryName);
                    category.setDisplayName(categoryName);
                    category.setAccessPermissions(permissions);
                }

                //
                ContentDefinition app = category.getContentMap().get(portletName);
                if (app == null) {
                    LocalizedString descriptionLS = metaInfo.getMetaValue(MetaInfo.DESCRIPTION);
                    LocalizedString displayNameLS = metaInfo.getMetaValue(MetaInfo.DISPLAY_NAME);
                    String displayName = getLocalizedStringValue(displayNameLS, portletName);

                    ContentType<?> contentType;
                    String contentId;
                    if (remote) {
                        contentType = WSRP.CONTENT_TYPE;
                        contentId = portlet.getContext().getId();
                        displayName += REMOTE_DISPLAY_NAME_SUFFIX; // add remote to display name to make it more obvious that
                                                                   // the portlet is remote
                    } else {
                        contentType = Portlet.CONTENT_TYPE;
                        contentId = info.getApplicationName() + "/" + info.getName();
                    }

                    // Check if the portlet has already existed in this category
                    List<Application> applications = load(category).getApplications();
                    boolean isExist = false;
                    for (Application application : applications) {
                        if (application.getContentId().equals(contentId)) {
                            isExist = true;
                            break;
                        }
                    }

                    if (!isExist) {
                        app = category.createContent(uniqueDefinitionName(portletName, contentId), contentType, contentId);
                        app.setDisplayName(displayName);
                        app.setDescription(getLocalizedStringValue(descriptionLS, portletName));
                        app.setAccessPermissions(permissions);
                    }
                }
            }
        }
    }

    private String uniqueDefinitionName(String portletName, String contentId) {
        String unique = portletName.substring(0,Math.min(portletName.length(), 20)) + String.valueOf(contentId.hashCode());
        String sanitized = sanitizePortletName(unique);
        return sanitized;
    }

    private String sanitizePortletApplicationName(String portletApplicationName) {
        return portletApplicationName.replace('/', '_');
    }

    private String sanitizePortletName(String portletName) {
        String sanitizedPortletName = portletName.replace('/', '_');
        /*
         * PortletName should validate similar to UIApplicationForm for Application Name
         * as this is read-only value in Application Registry.
         *
         * @see org.exoplatform.applicationregistry.webui.component.UIApplicationForm
         */
        int sizePortletName = sanitizedPortletName.length();
        if (sizePortletName >= MAX) {
            sanitizedPortletName = sanitizedPortletName.substring(0, MAX - 1);
        } else if (sizePortletName <= MIN) {
            sanitizedPortletName += "Portlet";

        }

        return sanitizedPortletName;
    }

    private String sanitizeCategoryName(String categoryName) {
        /*
         * CategoryName should validate similar to UIApplicationForm for Application Name
         * as this is read-only value in Application Registry.
         *
         * @see org.exoplatform.applicationregistry.webui.component.UIApplicationForm
         */
        int sizeCategoryName = categoryName.length();
        if (sizeCategoryName >= MAX) {
            categoryName = categoryName.substring(0, MAX - 1);
        } else if (sizeCategoryName <= MIN) {
            categoryName += "Category";
        }
        return categoryName;
    }

    private boolean isApplicationType(Application app, ApplicationType<?>... appTypes) {
        if (appTypes == null || appTypes.length == 0) {
            return true;
        }
        for (ApplicationType<?> appType : appTypes) {
            if (appType.equals(app.getType())) {
                return true;
            }
        }
        return false;
    }

    private void save(ApplicationCategory category, CategoryDefinition categoryDef) {
        categoryDef.setDisplayName(category.getDisplayName());
        categoryDef.setDescription(category.getDescription());
        categoryDef.setAccessPermissions(category.getAccessPermissions());
        categoryDef.setCreationDate(category.getCreatedDate());
        categoryDef.setLastModificationDate(category.getModifiedDate());
    }

    private ApplicationCategory load(CategoryDefinition categoryDef, ApplicationType<?>... appTypes) {
        ApplicationCategory category = new ApplicationCategory();

        //
        category.setName(categoryDef.getName());
        category.setDisplayName(categoryDef.getDisplayName());
        category.setDescription(categoryDef.getDescription());
        category.setAccessPermissions(new ArrayList<String>(categoryDef.getAccessPermissions()));
        category.setCreatedDate(categoryDef.getCreationDate());
        category.setModifiedDate(categoryDef.getLastModificationDate());

        //
        for (ContentDefinition contentDef : categoryDef.getContentList()) {
            Application application = load(contentDef);
            if (isApplicationType(application, appTypes)) {
                category.getApplications().add(application);
            }
        }

        //
        return category;
    }

    private void save(Application application, ContentDefinition contentDef) {
        contentDef.setDisplayName(application.getDisplayName());
        contentDef.setDescription(application.getDescription());
        contentDef.setAccessPermissions(application.getAccessPermissions());
        contentDef.setCreationDate(application.getCreatedDate());
        contentDef.setLastModificationDate(application.getModifiedDate());
    }

    private Application load(ContentDefinition contentDef) {
        Customization customization = contentDef.getCustomization();

        //
        ContentType<?> contentType = customization.getType();
        ApplicationType<?> applicationType = ApplicationType.getType(contentType);

        //
        Application application = new Application();
        /**
         * Apps ID has a "/" character that is rendered into id markup.
         * We need to workaround it to be W3C compliant.
         */
        application.setId(contentDef.getCategory().getName() + "_slash_" + contentDef.getName());
        application.setCategoryName(contentDef.getCategory().getName());
        application.setType(applicationType);
        application.setApplicationName(contentDef.getName());
        application.setIconURL(getApplicationIconURL(contentDef));
        application.setDisplayName(contentDef.getDisplayName());
        application.setDescription(contentDef.getDescription());
        application.setAccessPermissions(new ArrayList<String>(contentDef.getAccessPermissions()));
        application.setCreatedDate(contentDef.getCreationDate());
        application.setModifiedDate(contentDef.getLastModificationDate());
        application.setStorageId(customization.getId());
        application.setContentId(customization.getContentId());
        return application;
    }

    private String getLocalizedStringValue(LocalizedString localizedString, String portletName) {
        if (localizedString == null || localizedString.getDefaultString() == null) {
            return portletName;
        } else {
            return localizedString.getDefaultString();
        }
    }

    private static String getApplicationIconURL(ContentDefinition contentDef) {
        Customization customization = contentDef.getCustomization();
        if (customization != null) {
            ContentType type = customization.getType();
            String contentId = customization.getContentId();
            if (type == Portlet.CONTENT_TYPE) {
                String[] chunks = contentId.split("/");
                if (chunks.length == 2) {
                    return "/" + chunks[0] + "/skin/DefaultSkin/portletIcons/" + chunks[1] + ".png";
                }
            } else if (type == WSRP.CONTENT_TYPE) {
                return "/eXoResources/skin/sharedImages/Icon80x80/DefaultPortlet.png";
            } else if (type == org.exoplatform.portal.pom.spi.gadget.Gadget.CONTENT_TYPE) {
                return "/" + "eXoGadgets" + "/skin/DefaultSkin/portletIcons/" + contentId + ".png";
            }
        }

        //
        return null;
    }

    public void start() {
        if (plugins != null) {
            RequestLifeCycle.begin(manager);
            boolean save = false;
            try {
                if (this.getApplicationCategories().size() < 1) {
                    for (ApplicationCategoriesPlugins plugin : plugins) {
                        plugin.run();
                    }
                }
                save = true;
            } catch (Exception e) {
                log.error(e.getMessage(), e);
            } finally {
                // lifeCycle.closeContext(context, true);
                manager.getSynchronization().setSaveOnClose(save);
                RequestLifeCycle.end();
            }
        }
    }

    public void stop() {
    }
}
